using System;
using System.Text.RegularExpressions;

namespace ServiceStack.Translators.Generator.Support
{
	/// <summary>Implementation of a command-line parsing class.  Is capable of
	/// having switches registered with it directly or can examine a registered
	/// class for any properties with the appropriate attributes appended to
	/// them.</summary>
	public class Parser
	{
		/// <summary>A simple internal class for passing back to the caller
		/// some information about the switch.  The internals/implementation
		/// of this class has privillaged access to the contents of the
		/// SwitchRecord class.</summary>
		public class SwitchInfo
		{
			#region Private Variables
			private object m_Switch = null;
			#endregion

			#region Public Properties
			public string Name { get { return (m_Switch as SwitchRecord).Name; } }
			public string Description { get { return (m_Switch as SwitchRecord).Description; } }
			public string[] Aliases { get { return (m_Switch as SwitchRecord).Aliases; } }
			public System.Type Type { get { return (m_Switch as SwitchRecord).Type; } }
			public object Value { get { return (m_Switch as SwitchRecord).Value; } }
			public object InternalValue { get { return (m_Switch as SwitchRecord).InternalValue; } }
			public bool IsEnum { get { return (m_Switch as SwitchRecord).Type.IsEnum; } }
			public string[] Enumerations { get { return (m_Switch as SwitchRecord).Enumerations; } }
			#endregion

			/// <summary>
			/// Constructor for the SwitchInfo class.  Note, in order to hide to the outside world
			/// information not necessary to know, the constructor takes a System.Object (aka
			/// object) as it's registering type.  If the type isn't of the correct type, an exception
			/// is thrown.
			/// </summary>
			/// <param name="rec">The SwitchRecord for which this class store information.</param>
			/// <exception cref="ArgumentException">Thrown if the rec parameter is not of
			/// the type SwitchRecord.</exception>
			public SwitchInfo(object rec)
			{
				if (rec is SwitchRecord)
					m_Switch = rec;
				else
					throw new ArgumentException();
			}
		}

		/// <summary>
		/// The SwitchRecord is stored within the parser's collection of registered
		/// switches.  This class is private to the outside world.
		/// </summary>
		private class SwitchRecord
		{
			#region Private Variables
			private string m_name = "";
			private string m_description = "";
			private object m_value = null;
			private System.Type m_switchType = typeof(bool);
			private System.Collections.ArrayList m_Aliases = null;
			private string m_Pattern = "";

			// The following advanced functions allow for callbacks to be
			// made to manipulate the associated data type.
			private System.Reflection.MethodInfo m_SetMethod = null;
			private System.Reflection.MethodInfo m_GetMethod = null;
			private object m_PropertyOwner = null;
			#endregion

			#region Private Utility Functions
			private void Initialize(string name, string description)
			{
				m_name = name;
				m_description = description;

				BuildPattern();
			}

			private void BuildPattern()
			{
				string matchString = Name;

				if (Aliases != null && Aliases.Length > 0)
					foreach (string s in Aliases)
						matchString += "|" + s;

				string strPatternStart = @"(\s|^)(?<match>(-{1,2}|/)(";
				string strPatternEnd;  // To be defined below.

				// The common suffix ensures that the switches are followed by
				// a white-space OR the end of the string.  This will stop
				// switches such as /help matching /helpme
				//
				string strCommonSuffix = @"(?=(\s|$))";

				if (Type == typeof(bool))
					strPatternEnd = @")(?<value>(\+|-){0,1}))";
				else if (Type == typeof(string))
					strPatternEnd = @")(?::|\s+))((?:"")(?<value>.+)(?:"")|(?<value>\S+))";
				else if (Type == typeof(int))
					strPatternEnd = @")(?::|\s+))((?<value>(-|\+)[0-9]+)|(?<value>[0-9]+))";
				else if (Type.IsEnum)
				{
					string[] enumNames = Enumerations;
					string e_str = enumNames[0];
					for (int e=1; e < enumNames.Length; e++)
						e_str += "|" + enumNames[e];
					strPatternEnd = @")(?::|\s+))(?<value>" + e_str + @")";
				}
				else
					throw new System.ArgumentException();

				// Set the internal regular expression pattern.
				m_Pattern = strPatternStart + matchString + strPatternEnd + strCommonSuffix;
			}
			#endregion

			#region Public Properties
			public object Value
			{
				get
				{
					if (ReadValue != null)
						return ReadValue;
					else
						return m_value;
				}
			}

			public object InternalValue
			{
				get { return m_value; }
			}

			public string Name
			{
				get { return m_name; }
				set { m_name = value; }
			}

			public string Description
			{
				get { return m_description; }
				set { m_description = value; }
			}

			public System.Type Type
			{
				get { return m_switchType; }
			}

			public string[] Aliases
			{
				get { return (m_Aliases != null) ? (string[])m_Aliases.ToArray(typeof(string)) : null; }
			}

			public string Pattern
			{
				get { return m_Pattern; }
			}

			public System.Reflection.MethodInfo SetMethod
			{
				set { m_SetMethod = value; }
			}

			public System.Reflection.MethodInfo GetMethod
			{
				set { m_GetMethod = value; }
			}

			public object PropertyOwner
			{
				set { m_PropertyOwner = value; }
			}

			public object ReadValue
			{
				get
				{
					object o = null;
					if (m_PropertyOwner != null && m_GetMethod != null)
						o = m_GetMethod.Invoke(m_PropertyOwner, null);
					return o;
				}
			}

			public string[] Enumerations
			{
				get
				{
					if (m_switchType.IsEnum)
						return System.Enum.GetNames(m_switchType);
					else
						return null;
				}
			}
			#endregion

			#region Constructors
			public SwitchRecord(string name, string description)
			{
				Initialize(name, description);
			}

			public SwitchRecord(string name, string description, System.Type type)
			{
				if (type == typeof(bool) ||
				    type == typeof(string) ||
				    type == typeof(int) ||
				    type.IsEnum)
				{
					m_switchType = type;
					Initialize(name, description);
				}
				else
					throw new ArgumentException("Currently only Ints, Bool and Strings are supported");
			}
			#endregion

			#region Public Methods
			public void AddAlias(string alias)
			{
				if (m_Aliases == null)
					m_Aliases = new System.Collections.ArrayList();
				m_Aliases.Add(alias);

				BuildPattern();
			}

			public void Notify(object value)
			{
				if (m_PropertyOwner != null && m_SetMethod != null)
				{
					object[] parameters = new object[1];
					parameters[0] = value;
					m_SetMethod.Invoke(m_PropertyOwner, parameters);
				}
				m_value = value;
			}

			#endregion
		}


		#region Private Variables
		private string m_commandLine = "";
		private string m_workingString = "";
		private string m_applicationName = "";
		private string[] m_splitParameters = null;
		private System.Collections.ArrayList m_switches = null;
		#endregion

		#region Private Utility Functions
		private void ExtractApplicationName()
		{
			Regex r = new Regex(@"^(?<commandLine>("".+""|(\S)+))(?<remainder>.+)",
			                    RegexOptions.ExplicitCapture);
			Match m = r.Match(m_commandLine);
			if (m != null && m.Groups["commandLine"] != null)
			{
				m_applicationName = m.Groups["commandLine"].Value;
				m_workingString = m.Groups["remainder"].Value;
			}
		}

		private void SplitParameters()
		{
			// Populate the split parameters array with the remaining parameters.
			// Note that if quotes are used, the quotes are removed.
			// e.g.   one two three "four five six"
			//						0 - one
			//						1 - two
			//						2 - three
			//						3 - four five six
			// (e.g. 3 is not in quotes).
			Regex r = new Regex(@"((\s*(""(?<param>.+?)""|(?<param>\S+))))",
			                    RegexOptions.ExplicitCapture);
			MatchCollection m = r.Matches(m_workingString);

			if (m != null)
			{
				m_splitParameters = new string[m.Count];
				for (int i=0; i < m.Count; i++)
					m_splitParameters[i] = m[i].Groups["param"].Value;
			}
		}

		private void HandleSwitches()
		{
			if (m_switches != null)
			{
				foreach (SwitchRecord s in m_switches)
				{
					Regex r = new Regex(s.Pattern,
					                    RegexOptions.ExplicitCapture
					                    | RegexOptions.IgnoreCase);
					MatchCollection m = r.Matches(m_workingString);
					if (m != null)
					{
						for (int i=0; i < m.Count; i++)
						{
							string value = null;
							if (m[i].Groups != null && m[i].Groups["value"] != null)
								value = m[i].Groups["value"].Value;

							if (s.Type == typeof(bool))
							{
								bool state = true;
								// The value string may indicate what value we want.
								if (m[i].Groups != null && m[i].Groups["value"] != null)
								{
									switch (value)
									{
										case "+": state = true;
											break;
										case "-": state = false;
											break;
										case "": if (s.ReadValue != null)
											state = !(bool)s.ReadValue;
											break;
										default: break;
									}
								}
								s.Notify(state);
								break;
							}
							else if (s.Type == typeof(string))
								s.Notify(value);
							else if (s.Type == typeof(int))
								s.Notify(int.Parse(value));
							else if (s.Type.IsEnum)
								s.Notify(System.Enum.Parse(s.Type, value, true));
						}
					}

					m_workingString = r.Replace(m_workingString, " ");
				}
			}
		}

		#endregion

		#region Public Properties
		public string ApplicationName
		{
			get { return m_applicationName; }
		}

		public string[] Parameters
		{
			get { return m_splitParameters; }
		}

		public SwitchInfo[] Switches
		{
			get
			{
				if (m_switches == null)
					return null;
				else
				{
					SwitchInfo[] si = new SwitchInfo[m_switches.Count];
					for (int i=0; i < m_switches.Count; i++)
						si[i] = new SwitchInfo(m_switches[i]);
					return si;
				}
			}
		}

		public object this[string name]
		{
			get
			{
				if (m_switches != null)
					for (int i=0; i < m_switches.Count; i++)
						if (string.Compare((m_switches[i] as SwitchRecord).Name, name, true) == 0)
							return (m_switches[i] as SwitchRecord).Value;
				return null;
			}
		}

		/// <summary>This function returns a list of the unhandled switches
		/// that the parser has seen, but not processed.</summary>
		/// <remark>The unhandled switches are not removed from the remainder
		/// of the command-line.</remark>
		public string[] UnhandledSwitches
		{
			get
			{
				string switchPattern = @"(\s|^)(?<match>(-{1,2}|/)(.+?))(?=(\s|$))";
				Regex r = new Regex(switchPattern,
				                    RegexOptions.ExplicitCapture
				                    | RegexOptions.IgnoreCase);
				MatchCollection m = r.Matches(m_workingString);

				if (m != null)
				{
					string[] unhandled = new string[m.Count];
					for (int i=0; i < m.Count; i++)
						unhandled[i] = m[i].Groups["match"].Value;
					return unhandled;
				}
				else
					return null;
			}
		}
		#endregion

		#region Public Methods
		public void AddSwitch(string name, string description)
		{
			if (m_switches == null)
				m_switches = new System.Collections.ArrayList();

			SwitchRecord rec = new SwitchRecord(name, description);
			m_switches.Add(rec);
		}

		public void AddSwitch(string[] names, string description)
		{
			if (m_switches == null)
				m_switches = new System.Collections.ArrayList();
			SwitchRecord rec = new SwitchRecord(names[0], description);
			for (int s=1; s < names.Length; s++)
				rec.AddAlias(names[s]);
			m_switches.Add(rec);
		}

		public bool Parse()
		{
			ExtractApplicationName();

			// Remove switches and associated info.
			HandleSwitches();

			// Split parameters.
			SplitParameters();

			return true;
		}

		public object InternalValue(string name)
		{
			if (m_switches != null)
				for (int i=0; i < m_switches.Count; i++)
					if (string.Compare((m_switches[i] as SwitchRecord).Name, name, true) == 0)
						return (m_switches[i] as SwitchRecord).InternalValue;
			return null;
		}
		#endregion

		#region Constructors
		public Parser(string commandLine)
		{
			m_commandLine = commandLine;
		}

		public Parser(string commandLine,
		              object classForAutoAttributes)
		{
			m_commandLine = commandLine;

			Type type = classForAutoAttributes.GetType();
			System.Reflection.MemberInfo[] members = type.GetMembers();

			for (int i=0; i < members.Length; i++)
			{
				object[] attributes = members[i].GetCustomAttributes(false);
				if (attributes.Length > 0)
				{
					SwitchRecord rec = null;

					foreach (Attribute attribute in attributes)
					{
						if (attribute is CommandLineSwitchAttribute)
						{
							CommandLineSwitchAttribute switchAttrib =
								(CommandLineSwitchAttribute)attribute;

							// Get the property information.  We're only handling
							// properties at the moment!
							if (members[i] is System.Reflection.PropertyInfo)
							{
								System.Reflection.PropertyInfo pi = (System.Reflection.PropertyInfo)members[i];

								rec = new SwitchRecord(switchAttrib.Name,
								                       switchAttrib.Description,
								                       pi.PropertyType);

								// Map in the Get/Set methods.
								rec.SetMethod = pi.GetSetMethod();
								rec.GetMethod = pi.GetGetMethod();
								rec.PropertyOwner = classForAutoAttributes;

								// Can only handle a single switch for each property
								// (otherwise the parsing of aliases gets silly...)
								break;
							}
						}
					}

					// See if any aliases are required.  We can only do this after
					// a switch has been registered and the framework doesn't make
					// any guarantees about the order of attributes, so we have to
					// walk the collection a second time.
					if (rec != null)
					{
						foreach (Attribute attribute in attributes)
						{
							if (attribute is CommandLineAliasAttribute)
							{
								CommandLineAliasAttribute aliasAttrib =
									(CommandLineAliasAttribute)attribute;
								rec.AddAlias(aliasAttrib.Alias);
							}
						}
					}

					// Assuming we have a switch record (that may or may not have
					// aliases), add it to the collection of switches.
					if (rec != null)
					{
						if (m_switches == null)
							m_switches = new System.Collections.ArrayList();
						m_switches.Add(rec);
					}
				}
			}
		}
		#endregion
	}
}